/*
    Copyright © 2010  Ahti Legonkov
    
    This file is part of j+-.
    
    j+- is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.
    
    j+- is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.
    
    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

package cx.ath.skyflyer.javapm.visitor;

import cx.ath.skyflyer.javapm.util.Message;
import japa.parser.ast.CompilationUnit;
import japa.parser.ast.body.ClassOrInterfaceDeclaration;
import japa.parser.ast.body.MethodDeclaration;
import japa.parser.ast.body.ModifierSet;
import japa.parser.ast.expr.ArrayAccessExpr;
import japa.parser.ast.expr.ArrayCreationExpr;
import japa.parser.ast.expr.AssignExpr;
import japa.parser.ast.expr.BinaryExpr;
import japa.parser.ast.expr.CastExpr;
import japa.parser.ast.expr.ClassExpr;
import japa.parser.ast.expr.ConditionalExpr;
import japa.parser.ast.expr.DoubleLiteralExpr;
import japa.parser.ast.expr.EnclosedExpr;
import japa.parser.ast.expr.FieldAccessExpr;
import japa.parser.ast.expr.InstanceOfExpr;
import japa.parser.ast.expr.IntegerLiteralExpr;
import japa.parser.ast.expr.IntegerLiteralMinValueExpr;
import japa.parser.ast.expr.LongLiteralExpr;
import japa.parser.ast.expr.LongLiteralMinValueExpr;
import japa.parser.ast.expr.MarkerAnnotationExpr;
import japa.parser.ast.expr.MethodCallExpr;
import japa.parser.ast.expr.NameExpr;
import japa.parser.ast.expr.NullLiteralExpr;
import japa.parser.ast.expr.ObjectCreationExpr;
import japa.parser.ast.expr.QualifiedNameExpr;
import japa.parser.ast.expr.StringLiteralExpr;
import japa.parser.ast.expr.SuperExpr;
import japa.parser.ast.expr.ThisExpr;
import japa.parser.ast.expr.UnaryExpr;
import japa.parser.ast.expr.VariableDeclarationExpr;
import japa.parser.ast.type.ClassOrInterfaceType;
import japa.parser.ast.type.PrimitiveType;
import japa.parser.ast.type.ReferenceType;
import japa.parser.ast.type.Type;
import japa.parser.ast.visitor.GenericVisitorAdapter;


//public class Typeof {
//
//    private final TypeofVisitor tvi;
//    private final CompilationUnit cu;
//
//    public Typeof(CompilationUnit cu) {
//        this.cu = cu;
//        this.tvi = new TypeofVisitor(cu);
//    }
//
//    public Type getType(ExpressionStmt ex) {
//        return tvi.visit(ex, null);
//    }
//}

/**
 * This visitor is useful when determining the type of expression.
 * @author Ahti Legonkov
 */
public class TypeofVisitor extends GenericVisitorAdapter<Type,Object> {

    private final CompilationUnit compilationUnit;
    private MethodDeclaration currentMethod;
    private ClassOrInterfaceDeclaration currentClass;
    private Type type;

    public TypeofVisitor(CompilationUnit compilationUnit) {
        this.compilationUnit = compilationUnit;
    }

    public Type getType() {
        return type;
    }

    public void setCurrentClass(ClassOrInterfaceDeclaration cls) {
        currentClass = cls;
    }

    public void setCurrentMethod(MethodDeclaration method) {
        currentMethod = method;
    }
    
    @Override
    public Type visit(ArrayAccessExpr n, Object arg) {
        return type = n.getName().accept(this, arg);
    }

    @Override
    public Type visit(ArrayCreationExpr n, Object arg) {
        return type = new ReferenceType(n.getType(), n.getArrayCount());
    }

    @Override
    public Type visit(AssignExpr n, Object arg) {
        return type = n.getTarget().accept(this, arg);
    }

    private static boolean isString(Type t) {
        if (!(t instanceof ClassOrInterfaceType))
            return false;

        ClassOrInterfaceType rt = (ClassOrInterfaceType)t;
        return rt.getName().equals("String")
                || rt.getName().equals("java.lang.String");
    }

    private static boolean isDouble(Type t) {
        if (!(t instanceof PrimitiveType))
            return false;

        PrimitiveType pt = (PrimitiveType)t;
        return pt.getType() == PrimitiveType.Primitive.Double;
    }

    private static boolean isFloat(Type t) {
        if (!(t instanceof PrimitiveType))
            return false;

        PrimitiveType pt = (PrimitiveType)t;
        return pt.getType() == PrimitiveType.Primitive.Float;
    }

    private static boolean isLong(Type t) {
        if (!(t instanceof PrimitiveType))
            return false;

        PrimitiveType pt = (PrimitiveType)t;
        return pt.getType() == PrimitiveType.Primitive.Long;
    }

    // TODO: move this method out of this class!
    public static boolean isConvertible(Type src, Type dst) {
        // TODO:
        return true;
    }

    @Override
    public Type visit(BinaryExpr n, Object arg) {
        // JLS 4.2.2
        switch (n.getOperator()) {
        case or: // ||
        case and: // &&
            // 15.21.1
        case equals: // ==
        case notEquals: // !=
            // 15.20.1
        case less: // <
        case greater: // >
        case lessEquals: // <=
        case greaterEquals: // >=
            return type = new PrimitiveType(PrimitiveType.Primitive.Boolean);

        case binOr: // |
        case binAnd: // &
        case xor: // ^
        case lShift: // <<
        case rSignedShift: // >>
        case rUnsignedShift: // >>>
            return type = new PrimitiveType(PrimitiveType.Primitive.Int);
        }

        final Type left = n.getLeft().accept(this, arg);
        final Type right = n.getRight().accept(this, arg);

        switch (n.getOperator()) {
            // String, int, long, float or double
        case plus: // +
            if (isString(left)) return left;
            if (isString(right)) return right;
            // no break

            // int, long, float or double
        case minus: // -
        case times: // *
        case divide: // /
        case remainder: // %
            if (isDouble(left)) return type = left;
            if (isDouble(right)) return type = right;
            if (isFloat(left)) return type = left;
            if (isFloat(right)) return type = right;
            if (isLong(left)) return type = left;
            if (isLong(right)) return type = right;

            // smaller types are promoted to int
            return type = new PrimitiveType(PrimitiveType.Primitive.Int);
        }

        assert(false);
        return type = null;
    }

    @Override
    public Type visit(CastExpr n, Object arg) {
        return type = n.getType();
    }

    @Override
    public Type visit(ClassExpr n, Object arg) {
        return type = n.getType().accept(this, arg);
    }

    @Override
    public Type visit(ConditionalExpr n, Object arg) {
        final Type thenType = n.getThenExpr().accept(this, arg);
        final Type elseType = n.getElseExpr().accept(this, arg);

        if (thenType.equals(elseType))
            return type = thenType;

        if (isConvertible(thenType, elseType))
            return type = elseType;

        if (isConvertible(elseType, thenType))
            return type = thenType;

        // TODO: report error
        System.err.println("Error!");
        return type = null;
    }

    @Override
    public Type visit(DoubleLiteralExpr n, Object arg) {
        return type = new PrimitiveType(PrimitiveType.Primitive.Double);
    }

    @Override
    public Type visit(EnclosedExpr n, Object arg) {
        return type = n.getInner().accept(this, arg);
    }

    @Override
    public Type visit(FieldAccessExpr n, Object arg) {
        // TODO:
        // find type for n.getScope().
        // find n.getField() from that class.
        // return type of found field.
        return type = null;
    }

    @Override
    public Type visit(InstanceOfExpr n, Object arg) {
        return type = new PrimitiveType(PrimitiveType.Primitive.Boolean);
    }

    @Override
    public Type visit(IntegerLiteralExpr n, Object arg) {
        return type = new PrimitiveType(PrimitiveType.Primitive.Int);
    }

    @Override
    public Type visit(IntegerLiteralMinValueExpr n, Object arg) {
        return type = new PrimitiveType(PrimitiveType.Primitive.Int);
    }

    @Override
    public Type visit(LongLiteralExpr n, Object arg) {
        return type = new PrimitiveType(PrimitiveType.Primitive.Long);
    }

    @Override
    public Type visit(LongLiteralMinValueExpr n, Object arg) {
        return type = new PrimitiveType(PrimitiveType.Primitive.Long);
    }

    @Override
    public Type visit(MarkerAnnotationExpr n, Object arg) {
        //super.visit(n, arg);
        // TODO: ?
        return type = null;
    }

    @Override
    public Type visit(MethodCallExpr n, Object arg) {
        //super.visit(n, arg);
        // TODO: resolve method. return type of that method should be returned here
        return type = null;
    }

    @Override
    public Type visit(ClassOrInterfaceDeclaration n, Object arg) {
        final ClassOrInterfaceDeclaration prevClass = currentClass;
        currentClass = n;
        final Type res = super.visit(n, arg);
        currentClass = prevClass;
        return res;
    }

    @Override
    public Type visit(MethodDeclaration n, Object arg) {
        final MethodDeclaration prevMethod = currentMethod;
        currentMethod = n;
        final Type res = super.visit(n, arg);
        currentMethod = prevMethod;
        return res;
    }

    @Override
    public Type visit(NameExpr n, Object arg) {
        assert(currentMethod != null);
        assert(currentClass != null);

        // is n a local variable?
        FindLocalVariableVisitor flv = new FindLocalVariableVisitor(n);
        flv.visit(currentMethod, null);
        if (flv.isLocalVariable()) {
            // for primitive types, the readonly keyword acts as if it were
            // final keyword.
            VariableDeclarationExpr varDecl = flv.localVariable();
            type = varDecl.getType();
            final int varModifiers = varDecl.getModifiers();
            if (ModifierSet.isReadOnly(varModifiers)) {
                type.setModifiers(ModifierSet.addModifier(varModifiers, ModifierSet.READONLY));
            }
            return type;
        }

        // is n a parameter?
        //FindParameterVisitor fpv = new FindParameterVisitor(n);
        //fpv.

        // if n a class field?
        FindFieldVisitor ffv = new FindFieldVisitor(n.getName());
        ffv.visit(currentClass, arg);

        if (ffv.getField() != null) {
            // determine whether the field is static or not. it's ok to assign
            // to static fields in readonly method. it's not ok to assign to
            // non-static fields in readonly methods.
            return type = ffv.getField().getType();
        }
        // XXX:
        return type = null;
    }

    @Override
    public Type visit(NullLiteralExpr n, Object arg) {
        //super.visit(n, arg);
        return type = null;//"<anyref>";
    }

    @Override
    public Type visit(ObjectCreationExpr n, Object arg) {
        return type = n.getType();
    }

    @Override
    public Type visit(QualifiedNameExpr n, Object arg) {
        return type = null;//n.getName();
    }

    @Override
    public Type visit(StringLiteralExpr n, Object arg) {
        return type = new ClassOrInterfaceType("java.lang.String");
    }

    @Override
    public Type visit(SuperExpr n, Object arg) {
        super.visit(n, arg);
        // if extends list is empty
        //      return "java.lang.Object";
        //else
        //      return first element of extends list.
        return type = null;
    }

    @Override
    public Type visit(ThisExpr n, Object arg) {
        // TODO:
        return type = null;
    }

    @Override
    public Type visit(UnaryExpr n, Object arg) {
        return type = n.getExpr().accept(this, arg);
    }

    @Override
    public Type visit(VariableDeclarationExpr n, Object arg) {
        return type = n.getType();
    }
}
